using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace DotNetCRM.Core.ExtensionMethods
{
    public class ReflectionHelper
    {
        internal static readonly BindingFlags NonPublicFlags = BindingFlags.NonPublic | BindingFlags.Instance |
                                                               BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Static;

        internal static object InternalCallMethod(Type type, object instance, string name, 
                                                  Type[] genericParameters, params object[] parameters)
        {
            return InternalInvokeMember(type, instance, name, BindingFlags.InvokeMethod, genericParameters, parameters);
        }

        internal static void InternalSetField(Type type, object instance, string name, object value)
        {
            InternalInvokeMember(type, instance, name, BindingFlags.SetField, null, new[] { value });
        }

        internal static object InternalInvokeMember(Type type, object obj, string memberName,
                                                    BindingFlags flags, Type[] genericParameters, params object[] parameters)
        {
//            bool isMethod = (flags & BindingFlags.InvokeMethod) > 0;
//            flags = (isMethod) ? _nonPubFlags : flags | _nonPubFlags;
            flags = flags | NonPublicFlags;

            // .NET FX does not check if an empty string and just returns null making
            // it impossible to distinguish between an error or successful call that
            // returns void or null.
            memberName.CheckNullOrEmpty("memberName");

            // TODO Change this so the behavior is the same between generic and non-generic
            //	methods.  MethodInfo.Invoke() and type.InvokeMember() act differently
            //  when calling a method with variable arguments (params).  Make this change
            //  after unit tests are in place for all the InvokeXXX methods.
            object result = null;
            parameters = (parameters != null && parameters.Length > 0) ? parameters : null;
            genericParameters = (genericParameters != null && genericParameters.Length > 0) ? genericParameters : null;
            if (genericParameters != null)
            {
//                Type[] paramTypes = (parameters != null) 
//                    ? parameters.Select(p => p.GetType()).ToArray()
//                    : null;
                MethodInfo methodInfo = type.GetMethod(memberName, flags);
//                MethodInfo methodInfo = (paramTypes != null)
//                    ? type.GetMethod(memberName, flags, null, paramTypes, null)
//                    : type.GetMethod(memberName, flags);
                if (methodInfo != null)
                {
//                    if (genericParameters != null && genericParameters.Length > 0)
//                    {
                    methodInfo = methodInfo.MakeGenericMethod(genericParameters);
//                    }
                    result = methodInfo.Invoke(obj, parameters);
                }
            }
            else
            {
                result = type.InvokeMember(memberName, flags, null, obj, parameters);
            }
            return result;
        }

        internal static PropertyInfo InternalGetProperty(Type objectType, string propertyName, bool includeBaseTypePrivateProperties)
        {
            PropertyInfo pi = objectType.GetProperty(propertyName, NonPublicFlags);
            if (pi == null && objectType.BaseType != null && includeBaseTypePrivateProperties)
            {
                pi = InternalGetProperty(objectType.BaseType, propertyName, includeBaseTypePrivateProperties);
            }
            return pi; 
        }

        /// <summary>
        /// Find field by name, optionally including private fields in base types.
        /// </summary>
        /// <param name="objectType"></param>
        /// <param name="fieldName"></param>
        /// <param name="includeBaseTypePrivateFields"></param>
        /// <returns></returns>
        internal static FieldInfo InternalGetField(Type objectType, string fieldName, bool includeBaseTypePrivateFields)
        {
            FieldInfo fi = objectType.GetField(fieldName, NonPublicFlags);
            if (fi == null && objectType.BaseType != null && includeBaseTypePrivateFields)
            {
                fi = InternalGetField(objectType.BaseType, fieldName, includeBaseTypePrivateFields);
            }
            return fi;
        }

        /// <summary>
        /// Create an instance of the specified type.
        /// </summary>
        /// <typeparam name="T">Instance type to create and cast to.</typeparam>
        /// <param name="genericParameters">Generic parameter types.</param>
        /// <param name="args">Any constructor parameters.</param>
        /// <returns>The new object instance.</returns>
        public static T CreateInstance<T>(Type[] genericParameters, params object[] args)
        {
            return (T)CreateInstance(GetGenericType(typeof(T), genericParameters), args);
        }

        /// <summary>
        /// Binds generic paramters to a generic type returning a concrete type.
        /// </summary>
        /// <param name="type">Generic type</param>
        /// <param name="genericParameters">Generic parameter types</param>
        /// <returns>Returns the required concrete type, binding generic parameters if required.</returns>
        public static Type GetGenericType(Type type, params Type[] genericParameters)
        {
            if (genericParameters != null && genericParameters.Length > 0)
            {
                type = type.GetGenericTypeDefinition().MakeGenericType(genericParameters);
            }
            else
            {
                if (type.ContainsGenericParameters)
                    throw new ArgumentNullException("genericParameters", "genericParameters cannot " +
                                                                         "be null for unbound generic types");
            }
            return type;
        }

        /// <summary>
        /// Create an instance of the specified type.
        /// </summary>
        /// <param name="type">The type of the object to create.</param>
        /// <param name="genericParameters">genericParameters</param>
        /// <param name="args"></param>
        /// <returns>The new object instance.</returns>
        public static object CreateInstance(Type type, Type[] genericParameters, params object[] args)
        {
            return CreateInstance(GetGenericType(type, genericParameters), args);
        }

        /// <summary>
        /// Create an instance of the specified type.
        /// </summary>
        /// <param name="type">The type of the object to create.</param>
        /// <param name="args"></param>
        /// <returns>The new object instance.</returns>
        public static object CreateInstance(Type type, params object[] args)
        {
            BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic
                                 | BindingFlags.CreateInstance | BindingFlags.Instance;
            if (args != null && args.Length == 0)
            {
                args = null;
            }
            if (type.ContainsGenericParameters)
            {
                throw new ArgumentException(
                    "Cannot create instance of generic type with unbound generic parameters");
            }
            return Activator.CreateInstance(type, flags, null, args, null, null);
        }

        /// <summary>
        /// Finds types, that implement a particular interface, in loaded assemblies.
        /// </summary>
        /// <param name="interfaceType">the interface to search for</param>
        /// <returns>List of types</returns>
        /// <remarks>
        /// This method only searches loaded assemblies in the application domain.
        /// It will not load assemblies to perform search.
        /// </remarks>
        public static IList<Type> FindTypesImplementingInterface(Type interfaceType)
        {
            return FindTypesImplementingInterface(interfaceType, AppDomain.CurrentDomain.GetAssemblies());
        }

        /// <summary>
        /// Finds types, that implement a particular interface, in specified assemblies.
        /// </summary>
        /// <param name="interfaceType">the interface to search for</param>
        /// <param name="assemblies">Assemblies to search through</param>
        /// <returns>List of types, may be empty but never null.</returns>
        /// <remarks>
        /// This method only searches the specified assemblies.
        /// It will not load assemblies to perform search.
        /// </remarks>
        public static IList<Type> FindTypesImplementingInterface(Type interfaceType, params Assembly[] assemblies)
        {
            interfaceType.CheckNull("interfaceType");
            if (!interfaceType.IsInterface)
            {
                throw new ArgumentException("interfaceType is not an interface");
            }
            return FindTypes(interfaceType, ((x, y) => x.IsAssignableFrom(y)), assemblies);
        }

        /// <summary>
        /// Finds types, with a particular custom attribute defined, in loaded assemblies 
        /// </summary>
        /// <param name="attributeType">The custom attribute to search for</param>
        /// <returns>List of types, may be empty but never null.</returns>
        /// <remarks>
        /// This method only searches loaded assemblies in the application domain.
        /// It will not load assemblies to perform search.
        /// </remarks>
        public static IList<Type> FindTypesWithAttribute(Type attributeType)
        {
            return FindTypesWithAttribute(attributeType, AppDomain.CurrentDomain.GetAssemblies(), true);
        }

        /// <summary>
        /// Finds types, with a particular custom attribute defined, in loaded assemblies 
        /// </summary>
        /// <param name="attributeType">The custom attribute to search for</param>
        /// <param name="inherit">Specifies whether to search each member's inheritance chain to find the attribute.</param>
        /// <returns>List of types, may be empty but never null.</returns>
        /// <remarks>
        /// This method only searches loaded assemblies in the application domain.
        /// It will not load assemblies to perform search.
        /// </remarks>
        public static IList<Type> FindTypesWithAttribute(Type attributeType, bool inherit)
        {
            return FindTypesWithAttribute(attributeType, AppDomain.CurrentDomain.GetAssemblies(), inherit);
        }

        /// <summary>
        /// Finds types, with a particular custom attribute defined, in specified assemblies 
        /// </summary>
        /// <param name="attributeType">The custom attribute to search for</param>
        /// <param name="assemblies">Assemblies to search through</param>
        /// <param name="inherit">Specifies whether to search each member's inheritance chain to find the attribute.</param>
        /// <returns>List of types, may be empty but never null.</returns>
        /// <remarks>
        /// This method only searches loaded assemblies in the application domain.
        /// It will not load assemblies to perform search.
        /// </remarks>
        public static IList<Type> FindTypesWithAttribute(Type attributeType, IList<Assembly> assemblies, bool inherit)
        {
            attributeType.CheckNull("attributeType");
            assemblies.CheckNull("assemblies");
            if (!attributeType.IsSubclassOf(typeof(Attribute)))
            {
                throw new ArgumentException("Type must inherit from System.Attribute");
            }
            return FindTypes(attributeType, ((x, y) => !y.IsInterface && y.IsDefined(x, inherit)), assemblies);
        }

        private static IList<Type> FindTypes(Type type, Func<Type, Type, bool> func, IEnumerable<Assembly> assemblies)
        {
            assemblies.CheckNull("assemblies");

            IList<Type> types = new List<Type>();
            foreach (Assembly assembly in assemblies)
            {
                if (assembly == null)
                {
                    continue;
                }
                foreach (Type assemblyType in assembly.GetTypes())
                {
                    if (!assemblyType.IsAbstract && func(type, assemblyType))
                    {
                        types.Add(assemblyType);
                    }
                }
            }
            return types;
        }

        /// <summary>
        /// Adds an event handler to an event reflectively.
        /// </summary>
        /// <param name="eventObj">Instance of object on which the event is defined</param>
        /// <param name="eventName">Event name</param>
        /// <param name="delegateObj">Instance of object on which the delegate is defined</param>
        /// <param name="delegateName">Delegate method name</param>
        /// <remarks>
        /// If the delegateObj parameter is null, it will be assumed that the delegate
        /// is defined on the eventObj.
        /// </remarks>
        public static void AddEventHandler(object eventObj, string eventName,
                                           object delegateObj, string delegateName)
        {
            eventObj.CheckNull("eventObj");
            if (delegateObj == null)
            {
                delegateObj = eventObj;
            }
            eventName.CheckNullOrEmpty("eventName");
            delegateName.CheckNullOrEmpty("delegateName");
            // get a reference to the EventInfo for this object
            EventInfo eventInfo = eventObj.GetType().GetEvent(eventName);
            if (eventInfo != null && eventInfo.EventHandlerType != null)
            {
                Delegate delegateInstance = Delegate.CreateDelegate(eventInfo.EventHandlerType,
                                                                    delegateObj, delegateName, true);
                // make the local procedure a handler for the event
                eventInfo.AddEventHandler(eventObj, delegateInstance);
            }
        }

        /// <summary>
        /// Looks through all loaded assemblies for a type by its name.
        /// </summary>
        /// <param name="typeName">Full Type Name (e.g. Namespace.TypeName)</param>
        /// <returns>Matching type, or null if not found.</returns>
        public static Type FindTypeByFullName(string typeName)
        {
            return FindTypeByFullName(typeName, AppDomain.CurrentDomain.GetAssemblies());
        }

        /// <summary>
        /// Looks through all list of assemblies for a type by its name.
        /// </summary>
        /// <param name="typeName">Full Type Name (e.g. Namespace.TypeName)</param>
        /// <param name="assemblies">List of assemblies to search</param>
        /// <returns>Matching type, or null if not found.</returns>
        public static Type FindTypeByFullName(string typeName, IList<Assembly> assemblies)
        {
            return FindTypeByName(typeName, assemblies, true);
        }

        public static Type FindTypeByName(string typeName)
        {
            return FindTypeByName(typeName, AppDomain.CurrentDomain.GetAssemblies());
        }

        // Function left for the ObjectService, which assumes the type.Name 
        public static Type FindTypeByName(string typeName, IList<Assembly> assemblies)
        {
            return FindTypeByName(typeName, assemblies, false);
        }

        private static Type FindTypeByName(string typeName, IEnumerable<Assembly> assemblies, bool useFullName)
        {
            assemblies.CheckNull("assemblies");
            typeName.CheckNullOrEmpty("typeName");
            Type result = null;
            foreach (Assembly assembly in assemblies)
            {
                if (assembly == null)
                {
                    continue;
                }
                foreach (Type type in assembly.GetTypes())
                {
                    if (String.Compare(useFullName ? type.FullName : type.Name, typeName, true) == 0)
                    {
                        result = type;
                        break;
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// Create an instance of a type in the current assembly
        /// </summary>
        /// <typeparam name="T">Type to create</typeparam>
        /// <param name="className"></param>
        /// <returns></returns>
        public static T CreateInstance<T>(string className)
        {
            return (T)Assembly.GetExecutingAssembly().CreateInstance(className);
        }

        public static List<Assembly> GetAssembliesByName(IEnumerable<string> assemblyNames, bool throwExceptionIfNotFound)
        {
            List<Assembly> assemblies = new List<Assembly>();
            if (assemblyNames != null)
            {
                foreach (string assemblyName in assemblyNames)
                {
                    Assembly assembly = GetAssemblyByName(assemblyName);
                    if (throwExceptionIfNotFound && assembly == null)
                    {
                        throw new ApplicationException(string.Format("Assembly:'{0}' not found", assemblyName));
                    }
                    assemblies.Add(assembly);
                }
            }
            return assemblies;
        }

        public static Assembly GetAssemblyByName(string assemblyName)
        {
            // Check loaded Assemblies first. If assembly hasn't been loaded, load from local bin folder with
            // partial name.
            return (AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name == assemblyName)
                    ?? AppDomain.CurrentDomain.Load(assemblyName));
        }

        
        /// <summary>
        /// "Bayer.*.dll"
        /// </summary>
        /// <param name="assemblySearchPattern"></param>
        /// <returns></returns>
        public static List<Assembly> GetAssembliesFromBinFolder(string assemblySearchPattern)
        {
            List<Assembly> assemblies = new List<Assembly>();
            string binFolderPath = AppDomain.CurrentDomain.BaseDirectory;
            string[] files = assemblySearchPattern.IsNullOrEmptyTrimmed() 
                                 ? Directory.GetFiles(binFolderPath)
                                 : Directory.GetFiles(binFolderPath, assemblySearchPattern);
            if (files != null && files.Length > 0)
            {
                foreach(string assemblyFile in files)
                {
                    FileInfo fileInfo = new FileInfo(assemblyFile);
                    AssemblyName assemblyName = new AssemblyName();
                    assemblyName.Name = fileInfo.Name.Substring(0, fileInfo.Name.Length - 4);
                    assemblies.Add(Assembly.Load(assemblyName));
                }
            }
            return assemblies;
        }
    }
}